﻿using System;
using System.Diagnostics.Contracts;
using Primer.Storage;

namespace Primer
{
    internal enum PGHDR : ushort
    {
        DIRTY = 0x002,  /* Page has changed */
        NEED_SYNC = 0x004,  /* Fsync the rollback journal before writing this page to the database */
        NEED_READ = 0x008,  /* Content is unread */
        REUSE_UNLIKELY = 0x010,  /* A hint that reuse is unlikely */
        DONT_WRITE = 0x020,  /* Do not write content to disk */
    }

    public class Pcache
    {
        public static sqlite3_pcache_methods sqlite3GlobalConfig_pcache;
        //
        public PgHdr Dirty, DirtyTail;
        public PgHdr Synced;
        public int Ref;
        public int Max;
        public int PageSize;
        public int ExtraSize;
        public bool Purgeable;
        public Action<object, PgHdr> Stress;
        public object StressArg;
        public sqlite3_pcache Cache;
        public PgHdr Page1;

        public static void RemoveFromDirtyList(PgHdr page)
        {
            var p = page.Cache;
            //Contract.Assert(
            // Update the PCache1.pSynced variable if necessary.
            if (p.Synced == page)
            {
                var synced = page.DirtyPrev;
                while ((synced != null) && (0 != (synced.Flags & PGHDR.NEED_SYNC)))
                    synced = synced.DirtyPrev;
                p.Synced = synced;
            }
            if (page.DirtyNext != null)
                page.DirtyNext.DirtyPrev = page.DirtyPrev;
            else
                p.DirtyTail = page.DirtyPrev;
            if (page.DirtyPrev != null)
                page.DirtyPrev.DirtyNext = page.DirtyNext;
            else
                p.Dirty = page.DirtyNext;
            page.DirtyNext = null;
            page.DirtyPrev = null;
        }

        public static void AddToDirtyList(PgHdr page)
        {
            var p = page.Cache;
            page.DirtyNext = p.Dirty;
            if (page.DirtyNext != null)
                page.DirtyNext.DirtyPrev = page;
            p.Dirty = page;
            if (p.DirtyTail == null)
                p.DirtyTail = page;
            if ((p.Synced == null) && (0 == (page.Flags & PGHDR.NEED_SYNC)))
                p.Synced = page;
        }

        public static void Initialize()
        {
            if (sqlite3GlobalConfig_pcache != null)
                sqlite3GlobalConfig_pcache.Init();
            //sqlite3PCacheSetDefault();
        }

        public static void Shutdown()
        {
            if (sqlite3GlobalConfig_pcache != null)
                sqlite3GlobalConfig_pcache.Shutdown();
        }

        public static void Unpin(PgHdr p)
        {
            var cache = p.Cache;
            if (cache.Purgeable)
            {
                if (p.Pgno == 1)
                    cache.Page1 = null;
                sqlite3GlobalConfig_pcache.Unpin(cache.Cache, p, 0);
            }
        }

        public Pcache(int pageSize, int extraSize, bool purgeable, Action<object, PgHdr> stress, object stressArg)
        {
            PageSize = pageSize;
            ExtraSize = extraSize;
            Purgeable = purgeable;
            Stress = stress;
            StressArg = stressArg;
            Max = 100;
        }


        private void SetPageSize(int value)
        {
            if (Cache != null)
            {
                //sqlite3GlobalConfig_pcache.xDestroy(Cache);
                Cache = null;
                Page1 = null;
            }
            PageSize = value;
        }

        //private int Fetch(uint pgno, bool createFlag, ref PgHdr ppPage)
        //{
        //    PgHdr page = null;
        //    int eCreate;
        //    // If the pluggable cache (sqlite3_pcache*) has not been allocated, allocate it now.
        //    if ((Cache == null) && createFlag)
        //    {
        //        int bytes = PageSize + ExtraSize + sizeof(PgHdr);
        //        var p = sqlite3GlobalConfig_pcache.Create(bytes, Purgeable);
        //        if (p == null)
        //            rc = SQLITE_NOMEM;
        //        sqlite3GlobalConfig_pcache.Cachesize(p, Max);
        //        Cache = p;
        //    }
        //    eCreate = createFlag * (1 + (!Purgeable || (Dirty == null)));
        //    if (Cache != null)
        //        page = sqlite3GlobalConfig_pcache.Fetch(Cache, pgno, eCreate);
        //    if (!page && eCreate == 1)
        //    {
        //        PgHdr pPg;
        //        // Find a dirty page to write-out and recycle. First try to find a page that does not require a journal-sync (one with PGHDR_NEED_SYNC
        //        // cleared), but if that is not possible settle for any other unreferenced dirty page.
        //        for (pPg = Synced; (pPg != null) && ((pPg.Ref != 0) || ((pPg.Flags & PGHDR.NEED_SYNC) != 0)); pPg = pPg.DirtyPrev) { }
        //        Synced = pPg;
        //        if (pPg == null)
        //            for (pPg = DirtyTail; (pPg != null) && (pPg.Ref != 0); pPg = pPg.DirtyPrev) { }
        //        if (pPg != null)
        //        {
        //            int rc;
        //            rc = Stress(StressArg, pPg);
        //            if (rc != SQLITE_OK && rc != SQLITE_BUSY)
        //                return rc;
        //        }
        //        page = sqlite3GlobalConfig_pcache.xFetch(pCache->pCache, pgno, 2);
        //    }
        //    if (page)
        //    {
        //        if (!page->pData)
        //        {
        //            memset(page, 0, sizeof(PgHdr));
        //            page->pData = (void*)&page[1];
        //            page->pExtra = (void*)&((char*)page->pData)[pCache->szPage];
        //            memset(page->pExtra, 0, pCache->szExtra);
        //            page->pCache = pCache;
        //            page->pgno = pgno;
        //        }
        //        if (0 == page->nRef)
        //            pCache->nRef++;
        //        page->nRef++;
        //        if (pgno == 1)
        //            pCache->pPage1 = page;
        //    }
        //    ppPage = page;
        //    return ((page == 0) && eCreate ? SQLITE_NOMEM : SQLITE_OK);
        //}

        private static void Release(PgHdr p)
        {
            Contract.Assert(p.Ref > 0);
            p.Ref--;
            if (p.Ref == 0)
            {
                var cache = p.Cache;
                cache.Ref--;
                if ((p.Flags & PGHDR.DIRTY) == 0)
                    Unpin(p);
                else
                {
                    // Move the page to the head of the dirty list.
                    RemoveFromDirtyList(p);
                    AddToDirtyList(p);
                }
            }
        }

        private static void Ref2(PgHdr p)
        {
            p.Ref++;
        }

        private static void Drop(PgHdr p)
        {
            Contract.Assert(p.Ref == 1);
            if ((p.Flags & PGHDR.DIRTY) != 0)
                RemoveFromDirtyList(p);
            var cache = p.Cache;
            cache.Ref--;
            if (p.Pgno == 1)
                cache.Page1 = null;
            sqlite3GlobalConfig_pcache.Unpin(cache.Cache, p, 1);
        }

        private static void MakeDirty(PgHdr p)
        {
            p.Flags &= ~PGHDR.DONT_WRITE;
            Contract.Assert(p.Ref > 0);
            if ((p.Flags & PGHDR.DIRTY) == 0)
            {
                p.Flags |= PGHDR.DIRTY;
                AddToDirtyList(p);
            }
        }

        private static void MakeClean(PgHdr p)
        {
            if ((p.Flags & PGHDR.DIRTY) != 0)
            {
                RemoveFromDirtyList(p);
                p.Flags &= ~(PGHDR.DIRTY | PGHDR.NEED_SYNC);
                if (p.Ref == 0)
                    Unpin(p);
            }
        }

        private void CleanAll()
        {
            PgHdr p;
            while ((p = Dirty) != null)
                MakeClean(p);
        }

        private void ClearSyncFlags()
        {
            for (var p = Dirty; p != null; p = p.DirtyNext)
                p.Flags &= ~PGHDR.NEED_SYNC;
            Synced = DirtyTail;
        }

        private static void Move(PgHdr p, uint newPgno)
        {
            var cache = p.Cache;
            sqlite3GlobalConfig_pcache.Rekey(cache.Cache, p, p.Pgno, newPgno);
            p.Pgno = newPgno;
            if (((p.Flags & PGHDR.DIRTY) != 0) && ((p.Flags & PGHDR.NEED_SYNC) != 0))
            {
                RemoveFromDirtyList(p);
                AddToDirtyList(p);
            }
        }

        private void Truncate(uint pgno)
        {
            if (Cache != null)
            {
                PgHdr next;
                for (var p = Dirty; p != null; p = next)
                {
                    next = p.DirtyNext;
                    // This routine never gets call with a positive pgno except right after CleanAll().  So if there are dirty pages, it must be that pgno==0.
                    if (p.Pgno > pgno)
                        MakeClean(p);
                }
                if ((pgno == 0) && (Page1 != null))
                {
                    Array.Clear(Page1.Data, 0, PageSize);
                    pgno = 1;
                }
                sqlite3GlobalConfig_pcache.Truncate(Cache, pgno + 1);
            }
        }

        private void Close()
        {
            if (Cache != null)
                sqlite3GlobalConfig_pcache.Destroy(Cache);
        }

        private void Clear()
        {
            Truncate(0);
        }

        private static PgHdr MergeDirtyList(PgHdr pA, PgHdr pB)
        {
            var result = new PgHdr();
            var pTail = result;
            while ((pA != null) && (pB != null))
                if (pA.Pgno < pB.Pgno)
                {
                    pTail.Dirty = pA;
                    pTail = pA;
                    pA = pA.Dirty;
                }
                else
                {
                    pTail.Dirty = pB;
                    pTail = pB;
                    pB = pB.Dirty;
                }
            if (pA != null)
                pTail.Dirty = pA;
            else if (pB != null)
                pTail.Dirty = pB;
            else
                pTail.Dirty = null;
            return result.Dirty;
        }


        const int N_SORT_BUCKET = 32;
        private static PgHdr SortDirtyList(PgHdr pIn)
        {
            var a = new PgHdr[N_SORT_BUCKET];
            PgHdr p;
            //memset(a, 0, sizeof(a));
            while (pIn != null)
            {
                p = pIn;
                pIn = p.Dirty;
                p.Dirty = null;
                int i;
                for (i = 0; i < N_SORT_BUCKET - 1; i++)
                    if (a[i] == null)
                    {
                        a[i] = p;
                        break;
                    }
                    else
                    {
                        p = MergeDirtyList(a[i], p);
                        a[i] = null;
                    }
                if (i == N_SORT_BUCKET - 1)
                    // To get here, there need to be 2^(N_SORT_BUCKET) elements in the input list.  But that is impossible.
                    a[i] = MergeDirtyList(a[i], p);
            }
            p = a[0];
            for (int i = 1; i < N_SORT_BUCKET; i++)
                p = MergeDirtyList(p, a[i]);
            return p;
        }

        private PgHdr DirtyList()
        {
            for (var p = Dirty; p != null; p = p.DirtyNext)
                p.Dirty = p.DirtyNext;
            return SortDirtyList(Dirty);
        }


        private int RefCount()
        {
            return Ref;
        }

        private static int PageRefcount(PgHdr p)
        {
            return p.Ref;
        }

        private int Pagecount()
        {
            int nPage = 0;
            if (Cache != null)
                nPage = sqlite3GlobalConfig_pcache.Pagecount(Cache);
            return nPage;
        }

        private void SetCachesize(int mxPage)
        {
            Max = mxPage;
            if (Cache != null)
                sqlite3GlobalConfig_pcache.Cachesize(Cache, mxPage);
        }
    }
}
